package com.freewater.cargotracker.infrastructure.persistence.mybatis.repository;

import com.freewater.cargotracker.domain.model.cargo.*;
import com.freewater.cargotracker.domain.model.customer.Customer;
import com.freewater.cargotracker.domain.model.customer.CustomerNo;
import com.freewater.cargotracker.domain.model.customer.Role;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.converter.CargoConverter;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.converter.CustomerConverter;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.converter.LegConverter;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.dbobject.CargoCustomerPO;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.dbobject.CargoPO;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.dbobject.CustomerPO;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.dbobject.LegPO;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.CargoCustomerMapper;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.CargoHelperMapper;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.CargoMapper;
import com.freewater.cargotracker.infrastructure.persistence.mybatis.mapper.LegMapper;
import com.github.meixuesong.aggregatepersistence.Aggregate;
import com.github.meixuesong.aggregatepersistence.AggregateFactory;
import com.github.meixuesong.aggregatepersistence.ChangedEntity;
import com.github.meixuesong.aggregatepersistence.DataObjectUtils;
import org.springframework.stereotype.Repository;

import javax.persistence.OptimisticLockException;
import java.util.*;
import java.util.stream.Collectors;

@Repository
public class CargoRepositoryImpl implements CargoRepository {

    private final CargoMapper cargoMapper;
    private final CargoHelperMapper cargoHelperMapper;
    private final CargoCustomerMapper cargoCustomerMapper;
    private final LegMapper legMapper;

    public CargoRepositoryImpl(CargoMapper cargoMapper,
                               CargoHelperMapper cargoHelperMapper,
                               CargoCustomerMapper cargoCustomerMapper,
                               LegMapper legMapper) {
        this.cargoMapper = cargoMapper;
        this.cargoHelperMapper = cargoHelperMapper;
        this.cargoCustomerMapper = cargoCustomerMapper;
        this.legMapper = legMapper;
    }

    @Override
    public void save(Cargo cargo) {
        Long cargoId = cargo.getId();
        final CargoPO cargoPO = CargoConverter.serialize(cargo);
        if (cargoId == null) {
            cargoPO.setVersion(1);
            this.cargoMapper.insert(cargoPO);
            cargoId = cargoPO.getId();
            cargo.setId(cargoId);
        } else {
            final int rowCount = this.cargoMapper.update(cargoPO);
            if (rowCount != 1) {
                throw new RuntimeException("Concurrency update failed");
            }
            this.cargoCustomerMapper.deleteByCargoId(cargoId);
            this.legMapper.deleteByCargoId(cargoId);
        }
        final Map<String, CustomerPO> customers = cargoPO.getCustomers();
        this.insertCustomers(cargoId, customers);
        this.insertLegs(cargoId, cargo.getItinerary().getLegs());
    }

    @Override
    public void save(Aggregate<Cargo> cargoAggregate) {
        if (cargoAggregate.isNew()) {
            cargoAggregate.getRoot().setVersion(1);
            insertNewAggregate(cargoAggregate);
        } else {
            updateAggregateRoot(cargoAggregate);
            //
            insertCustomers(cargoAggregate);
            removeCustomers(cargoAggregate);
            //
            insertItineraryLegs(cargoAggregate);
            removeItineraryLegs(cargoAggregate);
            updateItineraryLegs(cargoAggregate);
        }
    }

    @Override
    public TrackingId nextTrackingId() {
        String random = UUID.randomUUID().toString().toUpperCase();
        return new TrackingId(random.substring(0, random.indexOf("-")));
    }

    @Override
    public Cargo findById(Long id) {
        final CargoPO cargoPO = this.cargoMapper.findById(id);
        if (cargoPO == null) {
            return null;
        }
        this.cargoHelperMapper.findCargoInfo(cargoPO);
        final Cargo cargo = CargoConverter.deserialize(cargoPO);
        return cargo;
    }

    @Override
    public Cargo findByTrackingId(TrackingId trackingId) {
        final CargoPO cargoPO = this.cargoMapper.findByTrackingId(trackingId.getIdString());
        if (cargoPO == null) {
            return null;
        }
        this.cargoHelperMapper.findCargoInfo(cargoPO);
        final Cargo cargo = CargoConverter.deserialize(cargoPO);
        return cargo;
    }

    @Override
    public Aggregate<Cargo> findAggregateByTrackingId(TrackingId trackingId) {
        final Cargo cargo = this.findByTrackingId(trackingId);
        if (cargo == null) {
            return null;
        }
        return AggregateFactory.createAggregate(cargo);
    }

    private void updateAggregateRoot(Aggregate<Cargo> cargoAggregate) {
        final Cargo cargo = cargoAggregate.getRoot();
        final Cargo snapshot = cargoAggregate.getRootSnapshot();
        final Set<String> changedFields = DataObjectUtils.getChangedFields(snapshot, cargo);
        // 注意，这里不管是否改变都需要修改version，否则乐观锁会失效
        final CargoPO cargoPO = CargoConverter.serialize(cargo);
        final int rowCount = this.cargoMapper.updateByIdSelective(cargoPO, changedFields);
        if (rowCount != 1) {
            throw new OptimisticLockException(String.format("Update cargo (%s) error, it's not found or changed by another user", cargo.getId()));
        }
    }

    private void insertCustomers(Aggregate<Cargo> cargoAggregate) {
        final Cargo cargo = cargoAggregate.getRoot();
        final Cargo snapshot = cargoAggregate.getRootSnapshot();
        final Map<Role, Customer> customers = cargo.getCargoCustomers().getCustomers();
        final Map<Role, Customer> customersSnapshot = snapshot.getCargoCustomers().getCustomers();
        final Map<Role, Customer> newCustomers = new HashMap<>();
        customers.forEach(((role, customer) -> {
            if (!customersSnapshot.containsKey(role)) {
                newCustomers.put(role, customer);
            }
        }));
        if (newCustomers.isEmpty()) {
            return;
        }
        final Map<String, CustomerPO> customerPOMap = new HashMap<>();
        newCustomers.forEach(((role, customer) -> customerPOMap.put(role.name(), CustomerConverter.serialize(customer))));
        this.insertCustomers(cargo.getId(), customerPOMap);
    }

    private void removeCustomers(Aggregate<Cargo> cargoAggregate) {
        final Cargo cargo = cargoAggregate.getRoot();
        final Cargo snapshot = cargoAggregate.getRootSnapshot();
        final Map<Role, Customer> customers = cargo.getCargoCustomers().getCustomers();
        final Map<Role, Customer> customersSnapshot = snapshot.getCargoCustomers().getCustomers();
        final Map<Role, Customer> removedCustomers = new HashMap<>();
        customersSnapshot.forEach(((role, customer) -> {
            if (!customers.containsKey(role)) {
                removedCustomers.put(role, customer);
            }
        }));
        if (removedCustomers.isEmpty()) {
            return;
        }
        final Map<String, CustomerPO> customerPOMap = new HashMap<>();
        removedCustomers.forEach(((role, customer) -> customerPOMap.put(role.name(), CustomerConverter.serialize(customer))));
        this.removeCustomers(cargo.getId(), customerPOMap);
    }

    private void insertItineraryLegs(Aggregate<Cargo> cargoAggregate) {
        final Collection<Leg> legs = cargoAggregate
                .findNewEntities(cargo -> cargo.getItinerary().getLegs(), leg -> leg.getId() == null);
        if (legs.isEmpty()) {
            return;
        }
        final Long cargoId = cargoAggregate.getRoot().getId();
        this.insertLegs(cargoId, new ArrayList<>(legs));
    }

    private void removeItineraryLegs(Aggregate<Cargo> cargoAggregate) {
        final Collection<Leg> legs = cargoAggregate
                .findRemovedEntities(cargo -> cargo.getItinerary().getLegs(), Leg::getId);
        if (legs.isEmpty()) {
            return;
        }
        legs.forEach(leg -> this.legMapper.deleteById(leg.getId()));
    }

    private void updateItineraryLegs(Aggregate<Cargo> cargoAggregate) {
        final Collection<ChangedEntity<Leg>> changedEntities = cargoAggregate
                .findChangedEntitiesWithOldValues(cargo -> cargo.getItinerary().getLegs(), Leg::getId);
        if (changedEntities.isEmpty()) {
            return;
        }
        final Long cargoId = cargoAggregate.getRoot().getId();
        for (ChangedEntity<Leg> changedEntity : changedEntities) {
            final Leg oldLeg = changedEntity.getOldEntity();
            final Leg newLeg = changedEntity.getNewEntity();
            final Set<String> changedFields = DataObjectUtils.getChangedFields(oldLeg, newLeg);
            final LegPO legPO = LegConverter.serialize(cargoId, newLeg);
            this.legMapper.updateByIdSelective(legPO, changedFields);
        }
    }

    private void insertNewAggregate(Aggregate<Cargo> cargoAggregate) {
        final CargoPO cargoPO = CargoConverter.serialize(cargoAggregate);
        this.cargoMapper.insert(cargoPO);
        final Cargo cargo = cargoAggregate.getRoot();
        cargo.setId(cargoPO.getId());
        //
        final Map<String, CustomerPO> customers = cargoPO.getCustomers();
        this.insertCustomers(cargo.getId(), customers);
        //
        final List<Leg> legs = cargo.getItinerary().getLegs();
        this.insertLegs(cargo.getId(), legs);
    }

    private void insertLegs(Long cargoId, List<Leg> legs) {
        final Map<LegPO, Leg> legMap = new HashMap<>();
        legs.forEach(leg -> {
            final LegPO legPO = LegConverter.serialize(cargoId, leg);
            legMap.put(legPO, leg);
        });
        if (!legMap.isEmpty()) {
            this.legMapper.insertAll(new ArrayList<>(legMap.keySet()));
            legMap.forEach(((legPO, leg) -> leg.setId(legPO.getId())));
        }
    }

    private void insertCustomers(Long cargoId, Map<String, CustomerPO> customers) {
        if (!customers.isEmpty()) {
            final List<CargoCustomerPO> customerPOs = customers.entrySet().stream()
                    .map(entry -> new CargoCustomerPO(cargoId, entry.getValue().getId(), entry.getKey()))
                    .collect(Collectors.toList());
            this.cargoCustomerMapper.insertAll(customerPOs);
        }
    }

    private void removeCustomers(Long cargoId, Map<String, CustomerPO> customers) {
        if (!customers.isEmpty()) {
            final List<CargoCustomerPO> customerPOs = customers.entrySet().stream()
                    .map(entry -> new CargoCustomerPO(cargoId, entry.getValue().getId(), entry.getKey()))
                    .collect(Collectors.toList());
            customerPOs.forEach(po -> this.cargoCustomerMapper.deleteByPrimaryKey(po));
        }
    }

    @Override
    public List<Cargo> findCargosOfCustomer(CustomerNo customerNo) {
        return null;
    }

    @Override
    public int quantityBooked(EnterpriseSegment enterpriseSegment) {
        return 0;
    }

}
